/*
 *	http://code.google.com/p/velleman-usb-experiment-board-tool/
 *
 *	Program for talking to Velleman(c) 8055 USB Experiment Interface Board
 *	Also known as: VM110, P8055, K8055
 *	TODO:
 *	-	Reset of switch 1 and 2 counts
 *	-	If Possible Change Read to use read of hiddev:
 *			[root@goflex usb]# dd bs=64 count=1 if=hiddev0 | od -X
 *			 0000000 ff000001 00000000 ff000002 00000001
 *			 0000020 ff000003 0000004d ff000004 0000009c
 *			 0000040 ff000005 00000000 ff000006 00000000
 *			 0000060 ff000007 00000000 ff000008 00000000
 *			 1+0 records in
 *			 1+0 records out
 *			 64 bytes (64 B) copied, 0.0051922 s, 12.3 kB/s
 *		This might be faster
 *
 *	USAGE:
 *		See Below
 *
 *	Ref: http://www.velleman.eu/products/view/?id=351346
 *
 *	$Id: v8055.c 14 2012-04-14 06:47:32Z cregganna@gmail.com $
 *
 *	Author: Tony.Jewell@Cregganna.Com
 *
 *	License: New BSD:
 *	Copyright (c) 2012, Tony.Jewell@Cregganna.Com
 *	All rights reserved.
 *
 *	Redistribution and use in source and binary forms, with or without
 *	modification, are permitted provided that the following conditions are
 *	met:
 *
 *	Redistributions of source code must retain the above copyright notice,
 *	this list of conditions and the following disclaimer.
 *	Redistributions in binary form must reproduce the above copyright
 *	notice, this list of conditions and the following disclaimer in the
 *	documentation and/or other materials provided with the distribution.
 *
 *	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 *	IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 *	TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *	PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *	HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *	LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
#include "libv8055.h"
#include <errno.h>
#include <string.h>
#include <getopt.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>

#define DEFAULT_DEVICE	"/dev/usb/hiddev0"
#define	DEFAULT_COUNT	"1"
#define	DEFAULT_PERIOD	"5"
#define DEFAULT_FORMAT	"%TS,%DI1,%DI1C,%DI2,%DI2C,%DI3,%DI4,%DI5,%AI1,%AI2,%DO1,%DO2,%DO3,%DO4,%DO5,%DO6,%DO7,%DO8,%AO1,%AO2"

#define USAGE "\
Interface with Velleman V8055 USB Experiment Interface Board\n\
%s [Options] [Field=Value [Field=Value]... ]\n\
Options:\n\
	-c nn	--count=nn	Set the number of reports to show\n\
				[Default=" DEFAULT_COUNT "]\n\
	-d dev	--device=dev	The Device that the board is registered on\n\
				[Default=" DEFAULT_DEVICE "]\n\
	-h	--skip-headings	Skip headings\n\
	-f fmt	--format=fmt	Set the Format for reports (See Below)\n\
				[Default=See below]\n\
	-p nn	--period=nn	Set the period between reports in seconds\n\
				[Default=" DEFAULT_PERIOD "]\n\
	-q	--skip-info	Skip Board Info lines\n\
	-r Field --read=Field	Read a Field and exit with its value\n\
				This will turn off reports (-c0 -h)\n\
	-?	--help		Report this help\n\
Report Format:\n\
	%% introduces a Field to report - supported Fields are:\n\
	Field	Description\n\
	%%	Put a '%%' in the report\n\
	TS	Time Stamp in Millis since epoch\n\
	DT	Date Time in ISO format (UTC)\n\
	DI	All Digital Inputs (Encoded as Bits)\n\
	DI1	Digital Input one (0=Off, 1=On)\n\
		Similarly for DI2, DI3, DI4, DI5\n\
	DI1C	Count of number of times Digital Input One has gone high\n\
	DI2C	Count of number of times Digital Input Two has gone high\n\
	AI1	Analog Input One\n\
	AI2	Analog Input Two\n\
	DO	All Digital Outputs (Encoded as Bits, Writable)\n\
	DO1	Digital Output One (0=Off, 1=On, Writable)\n\
		Similarly for DO2, DO3, DO4, DO5, DO6, DO7, DO8\n\
	AO1	Analog Output One (Writable)\n\
	AO2	Analog Output Two (Writable)\n\
"


#ifndef	FALSE
#define FALSE	0
#define TRUE	(!FALSE)
#endif

#define GROW(mem, need, index, size, grow_sz) \
	if ((index + need) > size) { \
		do { \
			size += grow_sz; \
		} while ((index + need) > size); \
		if (mem == 0) { \
			mem = malloc(sizeof(*mem) * size); \
		} else { \
			mem = realloc(mem, sizeof(*mem) * size); \
		} \
		if (mem == NULL) { \
			perror("Allocating memory for " #mem); \
			exit(EXIT_FAILURE); \
		} \
	}


struct v8055_value {
	char			*name;
	enum v8055_property	property;
	int			writable;		// Supports Output
	int			value;
	int			required;
};
//					    Name	Property			Writable
static struct v8055_value vv_di		= { "DI",	V8055_DIGITAL_INPUTS,		FALSE	};
static struct v8055_value vv_di1_count	= { "DI1C",	V8055_DIGITAL_INPUT1_COUNT,	FALSE	};
static struct v8055_value vv_di2_count	= { "DI2C",	V8055_DIGITAL_INPUT2_COUNT,	FALSE	};
static struct v8055_value vv_ai1	= { "AI1",	V8055_ANALOG_INPUT1,		FALSE	};
static struct v8055_value vv_ai2	= { "AI2",	V8055_ANALOG_INPUT2,		FALSE	};
static struct v8055_value vv_do		= { "DO",	V8055_DIGITAL_OUTPUTS,		TRUE	};
static struct v8055_value vv_ao1	= { "AO1",	V8055_ANALOG_OUTPUT1,		TRUE	};
static struct v8055_value vv_ao2	= { "AO2",	V8055_ANALOG_OUTPUT2,		TRUE	};
static struct v8055_value *v8055_values[] = {
	&vv_di, &vv_di1_count, &vv_di2_count, &vv_ai1, &vv_ai2,
	&vv_do, &vv_ao1, &vv_ao2,
	0
};

enum report_type {
	TIMESTAMP,
	DATETIME,
	LITERAL,	// In name
	V8055_PROPERTY,
};

struct v8055_param {
	char			*name;
	enum report_type	report_type;
	struct v8055_value	*vv;
	unsigned char		bit;
};
static struct v8055_param v8055_params[] = {
	// Be careful of order:
	// DI must follow DI1 otherwise it will be chosen in preference to it
	// Name		Report_Type	VV		Bit
	{ "%",		LITERAL,	NULL,		0	},
	{ "TS",		TIMESTAMP,	NULL,		0	},
	{ "DT",		DATETIME,	NULL,		0	},
	{ "DI1C",	V8055_PROPERTY,	&vv_di1_count,	0	},
	{ "DI1",	V8055_PROPERTY,	&vv_di,		0x01	},
	{ "DI2C",	V8055_PROPERTY,	&vv_di2_count,	0	},
	{ "DI2",	V8055_PROPERTY,	&vv_di,		0x02	},
	{ "DI3",	V8055_PROPERTY,	&vv_di,		0x04	},
	{ "DI4",	V8055_PROPERTY,	&vv_di,		0x08	},
	{ "DI5",	V8055_PROPERTY,	&vv_di,		0x10	},
	{ "DI",		V8055_PROPERTY,	&vv_di,		0	},
	{ "AI1",	V8055_PROPERTY,	&vv_ai1,	0	},
	{ "AI2",	V8055_PROPERTY,	&vv_ai2,	0	},
	{ "DO1",	V8055_PROPERTY,	&vv_do,		0x01	},
	{ "DO2",	V8055_PROPERTY,	&vv_do,		0x02	},
	{ "DO3",	V8055_PROPERTY,	&vv_do,		0x04	},
	{ "DO4",	V8055_PROPERTY,	&vv_do,		0x08	},
	{ "DO5",	V8055_PROPERTY,	&vv_do,		0x10	},
	{ "DO6",	V8055_PROPERTY,	&vv_do,		0x20	},
	{ "DO7",	V8055_PROPERTY,	&vv_do,		0x40	},
	{ "DO8",	V8055_PROPERTY,	&vv_do,		0x80	},
	{ "DO",		V8055_PROPERTY,	&vv_do,		0	},
	{ "AO1",	V8055_PROPERTY,	&vv_ao1,	0	},
	{ "AO2",	V8055_PROPERTY,	&vv_ao2,	0	},
	{ 0 }
};

static int failed = FALSE;
static char *v8055_device;
static long int report_count;
static long int report_period;
static int skip_headings = FALSE;
static int skip_info  = FALSE;
static int show_help = FALSE;
static struct v8055_param *reports;
static struct v8055_param *read_param;

static void fail()
{
	failed = TRUE;
}

/*
 * Options:
 */
static void set_count(char *count_string)
{
	char *end;
	report_count = strtol(count_string, &end, 10);
	if (*end != 0) {
		fprintf(stderr, "Invalid Count Argument: \"%s\"\n", count_string);
		fail();
	}
}

static void set_device(char *device_string)
{
	v8055_device = device_string;
}

static struct v8055_param *find_param_startswith(char *name)
{
	struct v8055_param *param;
	for (param = v8055_params; param->name != 0; param++) {
		if (strncmp(name, param->name, strlen(param->name)) == 0) {
			return param;
		}
	}
	return NULL;
}

static struct v8055_param *find_param(char *name, int length)
{
	struct v8055_param *param;
	for (param = v8055_params; param->name != 0; param++) {
		if (strncmp(name, param->name, length) == 0 && param->name[length] == 0) {
			return param;
		}
	}
	return NULL;
}

static void set_format(char *format_string)
{
	char	*last_pos = format_string;
	char	*pos = format_string;
	int	size = 0;
	int	index = 0;

	while ((pos = strchr(pos, '%')) != NULL) {
		if (last_pos != pos) {
			// Create a literal
			GROW(reports, 1, index, size, 20);
			int literal_len = pos - last_pos;
			reports[index].name = strndup(last_pos, literal_len);
			reports[index].report_type = LITERAL;
			index++;
		}
		pos++;
		struct v8055_param *param = find_param_startswith(pos);
		if (param == NULL) {
			fprintf(stderr, "Unrecognised Data Item at: %%%s\n", pos);
			fail();
			break;
		}
		GROW(reports, 1, index, size, 20);
		reports[index++] = *param;
		pos += strlen(param->name);
		last_pos = pos;
		if (param->vv != NULL) {
			param->vv->required = TRUE;
		}
	}
	if (*last_pos != 0) {
		// Create last literal
		GROW(reports, 1, index, size, 20);
		reports[index].name = strdup(last_pos);
		reports[index].report_type = LITERAL;
		index++;
	}
	GROW(reports, 1, index, size, 20);
	reports[index].name = 0;
}

static void set_period(char *period_string)
{
	char *end;
	report_period = strtol(period_string, &end, 10);
	if (*end != 0) {
		fprintf(stderr, "Invalid Period Argument: \"%s\"\n", period_string);
		fail();
	}
}

static void set_skip_headings()
{
	skip_headings = TRUE;
}

static void set_skip_info()
{
	skip_info = TRUE;
}

static void set_read(char *field_name)
{
	read_param = find_param(field_name, strlen(field_name));
	if (read_param == NULL) {
		fprintf(stderr, "Invalid Field to Read: \"%s\"\n", field_name);
		fail();
	}
	skip_headings = TRUE;
	report_count = 0;

}

static void set_show_help()
{
	show_help = TRUE;
}

static void do_help(char *program)
{
	printf(USAGE, program);
	printf("Default Format:\n%s\n", DEFAULT_FORMAT);
}

static int do_opts(int argc, char *argv[])
{
	static struct option long_options[] = {
		{"count",		required_argument,	0,	0 },
		{"device",		required_argument,	0,	0 },
		{"format",		required_argument,	0,	0 },
		{"period",		required_argument,	0,	0 },
		{"read",		required_argument,	0,	0 },
		{"skip-headings",	no_argument,		0,	0 },
		{"skip-info",		no_argument,		0,	0 },
		{"help",		no_argument,		0,	0 },
		{ 0 }
	};

	int c;
	int option_index = 0;

	while ( ( c = getopt_long(argc, argv, "c:d:f:p:r:hq?", long_options, &option_index) ) != -1) {

		switch (c) {
		case 0:
			switch(option_index) {
			case 0:	set_count(optarg); break;
			case 1: set_device(optarg); break;
			case 2: set_format(optarg); break;
			case 3: set_period(optarg); break;
			case 4: set_read(optarg); break;
			case 5: set_skip_headings(); break;
			case 6: set_skip_info(); break;
			case 7: set_show_help(); break;
			}
			break;
		
		case 'c':
			set_count(optarg);
			break;
		case 'd':
			set_device(optarg);
			break;
		case 'f':
			set_format(optarg);
			break;
		case 'p':
			set_period(optarg);
			break;
		case 'h':
			set_skip_headings();
			break;
		case 'q':
			set_skip_info();
			break;
		case 'r':
			set_read(optarg);
			break;
		case '?':
			if (opterr) {
				fail();
			}
			set_show_help();
			break;
		}
	}
	return optind;
}

/*
 * Arguments
 */
static void do_write(v8055 board, char *field_value)
{
	char *value_pos = strchr(field_value, '=');
	if (value_pos == NULL) {
		fprintf(stderr, "Invalid Write Parameter: \"%s\": Must be Field=Value where Field is a valid writable data item and Value is numeric\n", field_value);
		fail();
		return;
	}

	int fieldname_length = value_pos - field_value;
	struct v8055_param *param = find_param(field_value, fieldname_length);
	if (param == NULL) {
		fprintf(stderr, "Invalid Write Parameter: Cannot find Field: \"%.*s\": Must be Field=Value where Field is a Valid writable data item and value is numeric\n", fieldname_length, field_value);
		fail();
		return;
	}

	struct v8055_value *vv = param->vv;
	if (vv == NULL) {
		fprintf(stderr, "Invalid Write Parameter: \"%s\": Field is not a V8055 Property\n", field_value);
		fail();
		return;
	}
	if (!vv->writable) {
		fprintf(stderr, "Invalid Write Parameter: \"%s\": Field is not writable\n", field_value);
		fail();
		return;
	}
	value_pos++;
	unsigned value = strtol(value_pos, NULL, 10);
	if (param->bit != 0) {
		// This is a bit based property:
		int current_value = v8055_get_value(board, vv->property);
		if (current_value < 0) {
			fprintf(stderr, "Unable to read current value of %s: %s\n", vv->name, strerror(errno));
			fail();
			return;
		}
		if (value == 0) {
			// Switch Off
			value = (unsigned)current_value & ~param->bit;
		} else{
			// Switch On
			value = (unsigned)current_value | param->bit;
		}
	}
	if (v8055_put_value(board, vv->property, value) < 0) {
		fprintf(stderr, "Put Value %s: ", field_value);
		perror("Failed");
		fail();
	}
}

static void do_args(v8055 board, int start_args, int argc, char *argv[])
{
	int index;

	for (index = start_args; !failed && index < argc; index++) {
		do_write(board, argv[index]);
	}
}

/*
 * Getting and reporting the values
 */
static char *get_board_info(v8055 board)
{
	static char buf[100];

	struct v8055_board_info info;
	if (v8055_get_board_info(board, &info) < 0) {
		sprintf(buf, "Unknown: %.*s", (int)(sizeof(buf) - 20), strerror(errno));
	} else {
		sprintf(buf, "Vendor=%d, Product=%d, Version=%d, Name=%s", info.vendor_id, info.product_id, info.version, info.name);
	}

	return buf;

}

static void do_read(v8055 board)
{
	int value = v8055_get_value(board, read_param->vv->property);

	if (value < 0) {
		perror(read_param->name);
		value = EXIT_FAILURE;
	} else if (read_param->bit != 0) {
		value = (value & read_param->bit)? 1: 0;
	}

	exit(value);
}

static void do_headings()
{
	if (skip_headings) {
		return;
	}

	struct v8055_param *param;
	for (param = reports; param->name != 0; param++) {
		fputs(param->name, stdout);
	}
	putchar('\n');
}

static void get_values(v8055 board)
{
	struct v8055_value **vv;
	for (vv = v8055_values; *vv != NULL; vv++) {
		if ((*vv)->required) {
			(*vv)->value = v8055_get_value(board, (*vv)->property);
		}
	}
}

static void do_report_timestamp(struct timeval *timeval)
{
	printf("%ld%.3ld", timeval->tv_sec, timeval->tv_usec / 1000);
}

static void do_report_datetime(struct timeval *timeval)
{
	time_t now = timeval->tv_sec;
	struct tm *tm = gmtime(&now);
	printf("%.4d-%.2d-%.2dT%.2d:%.2d:%.2d.%.3ld",
		tm->tm_year + 1900,
		tm->tm_mon + 1,
		tm->tm_mday,
		tm->tm_hour,
		tm->tm_min,
		tm->tm_sec,
		timeval->tv_usec / 1000);
}

static void do_report_v8055_property(struct v8055_param *param)
{
	int value = param->vv->value;

	if (value < 0) {
		puts("ERR");
	} else if (param->bit == 0) {
		printf("%d", value);
	} else if ((value & param->bit) != 0) {
		putchar('1');
	} else {
		putchar('0');
	}
}

static void do_report(v8055 board)
{
	struct timeval timeval;
	gettimeofday(&timeval, NULL);
	get_values(board);

	int index;
	struct v8055_param *param;
	for (param = reports; param->name != 0; param++) {
		switch (param->report_type) {
		case LITERAL:
			fputs(param->name, stdout);
			break;
		case TIMESTAMP:
			do_report_timestamp(&timeval);
			break;
		case DATETIME:
			do_report_datetime(&timeval);
			break;
		case V8055_PROPERTY:
			do_report_v8055_property(param);
			break;
		}
	}
	putchar('\n');
}


/*
 * Main
 */
int main(int argc, char *argv[])
{
	set_device(DEFAULT_DEVICE);
	set_count(DEFAULT_COUNT);
	set_period(DEFAULT_PERIOD);
	if (reports == NULL) {
		set_format(DEFAULT_FORMAT);
	}

	int arg_index = do_opts(argc, argv);

	if (failed || show_help) {
		do_help(argv[0]);
		exit(failed? EXIT_FAILURE: 0);
	}

	v8055 board = v8055_open(v8055_device, FALSE);
	if (board == NULL) {
		fprintf(stderr, "%s", v8055_device);
		perror(": Unable to open");
		return -1;
	}

	if (!skip_info) {
		printf("Board: %s\n", get_board_info(board));
	}

	if (!v8055_validate(board)) {
		fprintf(stderr, "%s: Board not supported\n", v8055_device);
		exit(EXIT_FAILURE);
	}

	do_args(board, arg_index, argc, argv);
	if (failed) {
		do_help(argv[0]);
		exit(EXIT_FAILURE);
	}

	if (read_param != NULL) {
		do_read(board);
	}

	do_headings();

	int count;
	for (count = 0; count < report_count; count++) {
		if (count != 0) {
			sleep(report_period);
		}
		do_report(board);
	}

	exit(EXIT_SUCCESS);
}
